<?php
/*****************************************
 This file is part of the Dynamo Core CMS
 Copyright (c) Dynamo Studios 2008

 *** NOTE ***
 Permission is hereby granted to execute this file as a
 web service under the terms of the license agreement.

 See LICENSE.txt, in the system folder.

 All other rights, other than those expressed in the license
 document, are reserved.

 ****************************************/

/**
 * @name Security Services (ss)
 * @version 0.3.3 (Alvin)
 * @abstract
 * Provides security checks on objects via privilege levels
 * and access control lists.
 * 
 * Provides checking of client credentials and capabilities,
 * through the $gClient global variable (representing the current
 * client). 
 *
 * @author Sean Micklethwaite
 */

global $gClient;
global $gPrivString;
global $gSimplePrivs;
global $gSecurityObject;


//
// Bit offsets of security token
//

define("S_LIST", 0);
define("S_DISPLAY", 8);
define("S_MODERATE", 16);
define("S_MODIFY", 16);
define("S_ADMINISTRATE", 24);


//
// Security levels
//

define("BASIC", 0x10);
define("GENERAL", 0x20);
define("SECURE", 0x50);
define("ADMIN", 0x60);
define("SYSTEM", 0x70);
define("GOD", 0x80);

$gPrivString = array(0x00 => "0x00/PUBLIC",
					0x10 => "0x10/BASIC",
					0x20 => "0x20/GENERAL",
					0x30 => "0x30/ENHANCED",
					0x40 => "0x40/PROTECTED",
					0x50 => "0x50/SECURE",
					0x60 => "0x60/ADMIN",
					0x70 => "0x70/SYSTEM",
					0x80 => "0x80/GOD",
					0xFF => "0xFF/DENY");
$gSimplePrivs = array(0x00 => "Public (none)",
	0x10 => "Basic Member",
	0x40 => "Moderator",
	0x60 => "Administrator",
	0x80 => "System Operator");

//
// Access Control Flags
//

define('AC_GRANT',1);
define('AC_LIST',2);
define('AC_READ',4);
define('AC_WRITE',8);
define('AC_SPECIAL',0x10);
define('AC_FLAGS_MASK',0xFF);

define('AC_LVL_SHIFT',8);
define('AC_LVL_MASK',0xFF);



//
// Interface routines
//


function dyCheckToken($lvl, cDycmsObject &$obj)
/**
* @desc
* Performs a security check on a DyCMS Object.
* 
* @param $lvl	Security access type (read/write, etc.)
* @param $obj	Reference to object in question
*/
{
	global $gClient;

	if ($gClient->IsValidated()){
		//
		// Client is logged in. We must first load any ACEs
		// to check whether permission has been explicitly
		// granted or denied.
		//
		// We start by checking for ACEs on the current user,
		// then we iterate on the parent user until we reach
		// the top of the tree, or until we find an entry which
		// applies.
		//
		
		$acFlag = (2 << ($lvl >> 3));
		$depth = 0;
		for($client = $gClient; $client instanceof cDycmsClient;
				$client = $client->GetParent()){
			$ace = new cDycmsAccessCtrl($obj->GetClassNum(),
					$obj->GetId(), $client->GetUserNum());
			
			if($ace->DoesExist()){
				if($ace->IsDenied($acFlag, $gClient, $depth))
					return EDENIED;
				else if($ace->IsGranted($acFlag, $gClient, $depth))
					return true;
			};
		}; // for
		
		
		//
		// No applicable ACEs found - do privilege level check
		//
		
		return (($obj->GetOwnerNum() === $gClient->GetUserNum()) ||
			($gClient->GetPrivs() >= (0xFF & ($obj->GetToken() >> $lvl))))
			? true : EAUTH;
	}else
		return (0 === (0xFF & ($obj->GetToken() >> $lvl)))
			? true : EAUTH;
}

function dyValidateToken($lvl, cDycmsObject $obj)
/**
* @desc
*
*/
{
	global $gPrivString;

	$e = dyCheckToken($lvl, $obj);
	if($e === true)
		return true;
	else if($e === EAUTH)
		throw new dyExAuth("Insufficient privileges, required level is ".
			(isset($gPrivString[0xFF & ($obj->GetToken() >> $lvl)])?
				$gPrivString[0xFF & ($obj->GetToken() >> $lvl)]:
				(0xFF & ($obj->GetToken() >> $lvl))));
	else
		throw new dyExDenied();
}

function dyRequirePriv($priv)
/**
* @desc
* Checks the client privilege level is not below the supplied threshold,
* and throws an exception if it is.
*/
{
	global $gPrivString;
	
	if(!dyCheckPriv($priv))
		throw new dyExAuth("Required privilege level: $gPrivString[$priv]");
};

function dyCheckPriv($priv)
{
	global $gClient;

	if($priv == 0)
		return true;
	else if(!$gClient->IsValidated() || ($gClient->GetPrivs() < $priv))
		return false;
	else
		return true;
};

class cDycmsClient extends cDbClient
/**
 * Represents the logged in user
 **/
{
	private $mValidated = false;
	private	$mModified = false;

	function __construct($uNum = null)
	{
		if($uNum) {
			//
			// just load the data
			//

			$this->ForceRead($uNum);
			return;
		} else if($uNum === 0) {
			// Creating new
			$this->mszCapabilities = array();
			$this->mlUserNum = 0;
			return;
		};


		if(isset($_SESSION['ss-uidhash'])
			&& isset($_SESSION['ss-pwdhash']))
		{
			//
			// Try and load the existing client session
			//

			$this->mUserIdHash = $_SESSION['ss-uidhash'];
			$this->mPasswordHash = $_SESSION['ss-pwdhash'];
			$this->mValidated = $this->Read();
		};

		if($this->mValidated)
		{
			if(isset($_POST['$$ss-loginAction']) &&
					($_POST['$$ss-loginAction'] == 'logout'))
			{
				//
				// Close the client's session (logout)
				//

				session_unregister('ss-pwdhash');
				$this->mValidated = false;
			}
		}
		else
		{
			//
			// Check for pending logins
			//

			if(isset($_POST['$$ss-loginAction']) &&
					($_POST['$$ss-loginAction'] == 'login'))
			{
				$this->mUserId = $_POST['$$ss-uid'];
				$this->mUserIdHash = md5($this->mUserId);
				$this->mPasswordHash = md5($_POST['$$ss-pwd']);
				$this->mValidated = $this->Read();
			}
		}
	}

	function __destruct() {
		if($this->IsValidated())
		{
			//
			// Save login credentials in session
			//

			$_SESSION['ss-uidhash'] = $this->mUserIdHash;
			$_SESSION['ss-pwdhash'] = $this->mPasswordHash;
		};
	}

	public function Edit($uid, $name, $parent, $privs, $vz_caps, $pwd = null)
	/**
	 * Updates the client object with the passed new values, and saves the
	 * data to the database. This is a privileged operation, requiring ADMIN
	 * level privileges.
	 **/
	{
		dyRequirePriv(ADMIN);

		$this->mUserId = $uid;
		$this->mUserIdHash = md5($uid);
		if($pwd) $this->mPasswordHash = md5($pwd);
		else if($this->mlUserNum == 0)
			throw new dyExValidation("New users must be assigned a password");
		
		if($parent != 0){
			//
			// Validate the parent user number. If the user account isn't found,
			// this will throw an exception. We also check that we are not creating
			// an infinite loop.
			//
			$rParent = new cDycmsClient($parent);
	
			if(($this->mlUserNum > 0) && 
				$rParent->IsMemberOf($this->mlUserNum))
				throw new dyExValidation('This assignment would create a cycle, as '
					.$name.' is a superior of '.$rParent->GetUserName());
		};
		
		$this->mlParent = $parent;
		
		
		$this->mlPrivileges = $privs;
		$this->mszCapabilities = $vz_caps;
		$this->msName = $name;

		return $this->Write();
	}

	public function EditCredentials($uid, $name, $pwd = null)
	/**
	 * Changes a user's 'credentials': that is user name, display name,
	 * and optionally password. This can only be performed by the current
	 * user on their own account.
	 **/
	{
		global $gClient;
		
		if($gClient->IsValidated() &&
			($gClient->GetUserNum() == $this->GetUserNum()))
		{
			$this->mUserId = $uid;
			$this->mUserIdHash = md5($uid);
			if($pwd) $this->mPasswordHash = md5($pwd);
			
			$this->msName = $name;
			
			return $this->Write();
			
		}else
			throw new dyExDenied("Cannot change another user's credentials.");
	}

	public function Delete() {
		global $gClient;

		if($gClient->GetUserId() == $this->GetUserId())
			throw new dyExInvalidOp('Cannot delete the currently logged in user');
		else
			return $this->DoDelete();
	}

	public function IsMemberOf($unum){
		if($this->mlUserNum == $unum)
			return true;

		$parent = $this->GetParent();
		if($parent instanceof cDycmsClient)
			return $parent->IsMemberOf($unum);
		else
			return false;
	}
	
	public function CheckPrivileges($l_priv){
		return ($l_priv <= $this->mlPrivileges);
	}
	public function GetPrivs(){
		return $this->mlPrivileges;
	}
	public function GetPrivilegesString()
	{
		global $gPrivString;
		return $gPrivString[$this->mlPrivileges];
	}

	public function GetCapabilities(){
		return $this->mszCapabilities;
	}
    public function CheckCapability($cap){
        return (array_search($cap, $this->mszCapabilities) !== false);
    }
	public function IsValidated(){
		return $this->mValidated;
	}
	public function IsGuest(){
		return ($this->mlUserNum == GUEST_UNUM);
	}
	public function GetUid(){
		return $this->mUserIdHash;
	}
	public function GetUserId(){
		return $this->mUserId;
	}
	public function GetUserNum(){
		return $this->mlUserNum;
	}
	public function GetParentNum(){
		return $this->mlParent;
	}
	public function GetParent(){
		if($this->mlParent)
			return new cDycmsClient($this->mlParent);
		else return null;
	}
	public function GetUserName(){
		return $this->msName;
	}
}

class cDycmsAccessCtrl extends cDbAccessCtrl
{
	protected $mbExists;
	
	function __construct($cNum, $oId, $uNum){
		$this->miClassNum = $cNum;
		$this->miObjectId = $oId;
		$this->miUserNum = $uNum;
		
		// Try to read
		if(!$this->Read()){
			//
			// Read failed, create new
			//
			$this->mType = AC_GRANT;
		}
	}
	
	function CalcDepth(cDycmsClient $client){
		for($depth = 0,	$cc = $client;
				$cc->GetUserNum() != $this->mlUserNum;
				$depth++){

			$cc = $cc->GetParent();
			if(!($cc instanceof cDycmsClient))
				return false;
		};
	}
	
	function IsGranted($acFlag, cDycmsClient $client, $depth = null){
		if(!($this->mType & AC_GRANT))
			return false;
		
		if($depth === null){
			// Calculate depth
			$depth = $this->CalcDepth($client);
			if($depth === false)
				// Outside domain - not applicable
				return false;
		};
		
		
		//
		// Check that the user lies within the max depth
		//
		
		if($depth <= (($this->mType >> AC_LVL_SHIFT) & AC_LVL_MASK)){
			//
			// User in domain, check flags
			// Note: a 1 indicates the privilege granted
			//
			
			return ($this->mType & $acFlag);
		}else{
			// Over maximum depth - not applicable
			return false;
		};
	}
	
	function IsDenied($acFlag, cDycmsClient $client, $depth = null){
		if($this->mType & AC_GRANT)
			return false;
		
		if($client->CheckCapability('aclNoDeny'))
			return false;
		
		if($depth === null){
			// Calculate depth
			$depth = $this->CalcDepth($client);
			if($depth === false)
				// Outside domain - not applicable
				return false;
		};
		
		//
		// Check that the user lies over the min depth
		//
		
		if($depth >= (($this->mType >> AC_LVL_SHIFT) & AC_LVL_MASK)){
			//
			// User in domain, check flags
			// Note: a 1 indicates the privilege NOT being
			// denied
			//
			
			return !($this->mType & $acFlag);
		}else{
			// Under minimum depth - not applicable
			return false;
		};
	}
	
	function DoesExist(){
		return $this->mbExists;
	}
}

function & dyLoadClient($unum){
	// TODO: memoize
	return new cDycmsClient(intval($unum));
}

function dyInitSecurity()
{
	global $gClient;
	$gClient = new cDycmsClient();
};

function dyEndSecurity()
{
	global $gClient;
	unset($gClient);
};

?>
